package com.scpii.api.common.auth.token;

import java.util.Collection;
import java.util.Date;
import java.util.UUID;

import javax.annotation.Resource;

import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import com.scpii.api.common.auth.AuthorizationRequest;
import com.scpii.api.common.auth.ClientAuthentication;
import com.scpii.api.common.exception.InvalidTokenException;
import com.scpii.api.domain.auth.UsersInfoDetails;

@Component
public class DefaultTokenServices implements AuthorizationServerTokenServices,
		ResourceServerTokenServices {

	private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30 * 3;

	private int accessTokenValiditySeconds = 60 * 60 * 24 * 30 * 3;

	private boolean supportRefreshToken = false;

	private boolean reuseRefreshToken = true;

	@Resource(name = "defaultTokenStore")
	private TokenStore tokenStore;

	private TokenEnhancer accessTokenEnhancer;

	public void afterPropertiesSet() throws Exception {
		Assert.notNull(tokenStore, "tokenStore must be set");
	}

	public AuthAccessToken createAccessToken(ClientAuthentication authentication)
			throws AuthenticationException {

		AuthAccessToken existingAccessToken = tokenStore
				.getAccessToken(authentication);
		AuthRefreshToken refreshToken = null;
		if (existingAccessToken != null) {
			if (existingAccessToken.isExpired()) {
				if (existingAccessToken.getRefreshToken() != null) {
					refreshToken = existingAccessToken.getRefreshToken();
					tokenStore.removeRefreshToken(refreshToken);
				}
				tokenStore.removeAccessToken(existingAccessToken);
			} else {
				return existingAccessToken;
			}
		}

		if (refreshToken == null) {
			refreshToken = createRefreshToken(authentication);
		} else if (refreshToken instanceof ExpiringAuthRefreshToken) {
			ExpiringAuthRefreshToken expiring = (ExpiringAuthRefreshToken) refreshToken;
			if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
				refreshToken = createRefreshToken(authentication);
			}
		}

		AuthAccessToken accessToken = createAccessToken(authentication,
				refreshToken);
		tokenStore.storeAccessToken(accessToken, authentication);
		if (refreshToken != null) {
			tokenStore.storeRefreshToken(refreshToken, authentication);
		}
		return accessToken;

	}

	public AuthAccessToken refreshAccessToken(String refreshTokenValue,
			AuthorizationRequest request) throws AuthenticationException {
		if (!supportRefreshToken) {
			throw new InvalidTokenException("Invalid refresh token: "
					+ refreshTokenValue);
		}

		AuthRefreshToken refreshToken = tokenStore
				.readRefreshToken(refreshTokenValue);
		if (refreshToken == null) {
			throw new InvalidTokenException("Invalid refresh token: "
					+ refreshTokenValue);
		}

		ClientAuthentication authentication = tokenStore
				.readAuthenticationForRefreshToken(refreshToken);
		String clientId = authentication.getAuthorizationRequest().getAppId();
		if (clientId == null || !clientId.equals(request.getAppId())) {
			throw new InvalidTokenException(
					"Wrong client for this refresh token: " + refreshTokenValue);
		}

		tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);

		if (isExpired(refreshToken)) {
			tokenStore.removeRefreshToken(refreshToken);
			throw new InvalidTokenException("Invalid refresh token (expired): "
					+ refreshToken);
		}
		authentication = createRefreshedAuthentication(authentication);

		if (!reuseRefreshToken) {
			tokenStore.removeRefreshToken(refreshToken);
			refreshToken = createRefreshToken(authentication);
		}

		AuthAccessToken accessToken = createAccessToken(authentication,
				refreshToken);
		tokenStore.storeAccessToken(accessToken, authentication);
		if (!reuseRefreshToken) {
			tokenStore.storeRefreshToken(refreshToken, authentication);
		}
		return accessToken;
	}

	public AuthAccessToken getAccessToken(ClientAuthentication authentication) {
		return tokenStore.getAccessToken(authentication);
	}

	private ClientAuthentication createRefreshedAuthentication(
			ClientAuthentication authentication) {
		ClientAuthentication narrowed = authentication;
		return narrowed;
	}

	protected boolean isExpired(AuthRefreshToken refreshToken) {
		if (refreshToken instanceof ExpiringAuthRefreshToken) {
			ExpiringAuthRefreshToken expiringToken = (ExpiringAuthRefreshToken) refreshToken;
			return expiringToken.getExpiration() == null
					|| System.currentTimeMillis() > expiringToken
							.getExpiration().getTime();
		}
		return false;
	}

	public AuthAccessToken readAccessToken(String accessToken) {
		return tokenStore.readAccessToken(accessToken);
	}

	public ClientAuthentication loadAuthentication(String accessTokenValue)
			throws AuthenticationException {
		AuthAccessToken accessToken = tokenStore
				.readAccessToken(accessTokenValue);
		if (accessToken == null) {
			throw new InvalidTokenException("Invalid access token: "
					+ accessTokenValue);
		} else if (accessToken.isExpired()) {
			tokenStore.removeAccessToken(accessToken);
			throw new InvalidTokenException("Access token expired: "
					+ accessTokenValue);
		}

		ClientAuthentication result = tokenStore
				.readAuthentication(accessToken);
		return result;
	}

	public Collection<AuthAccessToken> findTokensByClientId(String clientId) {
		return tokenStore.findTokensByClientId(clientId);
	}

	public boolean revokeToken(String tokenValue) {
		AuthAccessToken accessToken = tokenStore.readAccessToken(tokenValue);
		if (accessToken == null) {
			return false;
		}
		if (accessToken.getRefreshToken() != null) {
			tokenStore.removeRefreshToken(accessToken.getRefreshToken());
		}
		tokenStore.removeAccessToken(accessToken);
		return true;
	}

	private ExpiringAuthRefreshToken createRefreshToken(
			ClientAuthentication authentication) {
		if (!isSupportRefreshToken(authentication.getAuthorizationRequest())) {
			return null;
		}
		int validitySeconds = getRefreshTokenValiditySeconds(authentication
				.getAuthorizationRequest());
		ExpiringAuthRefreshToken refreshToken = new DefaultExpiringAuthRefreshToken(
				UUID.randomUUID().toString(), new Date(
						System.currentTimeMillis() + (validitySeconds * 1000L)));
		return refreshToken;
	}

	private AuthAccessToken createAccessToken(
			ClientAuthentication authentication, AuthRefreshToken refreshToken) {
		DefaultAccessToken token = new DefaultAccessToken(UUID.randomUUID()
				.toString());
		int validitySeconds = getAccessTokenValiditySeconds(authentication
				.getAuthorizationRequest());
		if (validitySeconds > 0) {
			token.setExpiration(new Date(System.currentTimeMillis()
					+ (validitySeconds * 1000L)));
		}
		token.setRefreshToken(refreshToken);
		Object principal = authentication.getPrincipal();
		if (principal instanceof UsersInfoDetails) {
			token.setUsersInfoDetails((UsersInfoDetails) principal);
		}
		return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token,
				authentication) : token;
	}

	protected int getAccessTokenValiditySeconds(
			AuthorizationRequest authorizationRequest) {
		return accessTokenValiditySeconds;
	}

	protected int getRefreshTokenValiditySeconds(
			AuthorizationRequest authorizationRequest) {
		return refreshTokenValiditySeconds;
	}

	protected boolean isSupportRefreshToken(
			AuthorizationRequest authorizationRequest) {

		return this.supportRefreshToken;
	}

	public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) {
		this.accessTokenEnhancer = accessTokenEnhancer;
	}

	public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) {
		this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
	}

	public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {
		this.accessTokenValiditySeconds = accessTokenValiditySeconds;
	}

	public void setSupportRefreshToken(boolean supportRefreshToken) {
		this.supportRefreshToken = supportRefreshToken;
	}

	public void setReuseRefreshToken(boolean reuseRefreshToken) {
		this.reuseRefreshToken = reuseRefreshToken;
	}
}
